
# cameradevice.py
import ctypes
import json
from ctypes import byref, c_int, c_char_p
from asi_structs import ASI_CAMERA_INFO,ASI_CONTROL_CAPS,CameraStates,ASI_BAYER_PATTERN,ASI_IMG_TYPE,ASI_CONTROL_TYPE
# 可以从 .h 翻译的结构体文件中引入
from asi_structs import ASI_CONTROL_TYPE,ASI_BOOL,ASI_EXPOSURE_STATUS,ASI_GUIDE_DIRECTION,SensorType
import time
import threading
import datetime
import queue

from alpaca.exceptions import InvalidOperationException


'''

Q1:默认只连接了1台设备，所以都没怎么用到请求中传入的devnum这个参数，
    实际上作为一个小的服务端，还是有可能连接到同类型的多个设备，这时就需要devnum参数。暂时不做修改。

检查回顾：
（1）alpaca中CAN开头的属性。参照着SDK中的
    ASI_CONTROL_TYPE/struct _ASI_CONTROL_CAPS/struct _ASI_CAMERA_INFO
    例：相机有脉冲导星功能canpulseguide，但是我们不打算使用（因为本质上还是控制赤道仪，不同的是通过相机发出控制信号还是控制台发送）
    所以还是直接返回FALSE算了，主要是针对当前设备进行快速开发
（2）检查连接相关的代码，确保是异步实现
（3）检查BIN/NUM/START，即分辨率相关的代码。
    确保能够正确修改相关参数（避免再出现：改了参数但是没作用的结果）
    
    注：要确保iWidth%8=0，iHeight%2=0。 对于USB2.0相机ASI120，确保iWidth* iHeight%1024=0，否则不能设置成功。
    
（4）曝光相关的参数
    确保能够正常获取曝光参数且正常读取数据
（5）连续曝光的实现
（6）初始化状态以及初始化流程需要明确

'''


class ZwoCamera:
    def __init__(self, dll_path=r'lib/ASICamera2.dll'):
        # dll_path = r'lib/ASICamera2.dll'
        self.sdk = ctypes.cdll.LoadLibrary(dll_path)
        self.camera_id = 0  # 默认使用第一个相机
        self.connected = False
        self._connecting = False

        self._state = CameraStates.cameraIdle
        self._bin_x = 1
        self._bin_y = 1

        self.img_type = ASI_IMG_TYPE.ASI_IMG_RAW8
        self.info = ASI_CAMERA_INFO()  # 结构体：相机信息

        self._num_x = self.info.MaxWidth
        self._num_y = self.info.MaxHeight
        self._start_x = 0
        self._start_y = 0

        self.date = None

        self.control_caps = []  # 控制项列表
        self.image_type = 0  # 图像类型（如 ASI_IMG_RAW8）

        self._is_pulse_guiding = False
        self._pulse_end_time = 0

        self._polling_active = False  # 单次曝光时开启轮询
        self._exposure_start_time = time.time()
        self._exposure_start_time1 = datetime.datetime.utcnow()
        self._exposure_end_time = time.time()

        self.image_width = None
        self.image_height = None
        self.img_type = ASI_IMG_TYPE.ASI_IMG_RAW16
        self.exposure_timeout_ms = 1000
        self.image_ready_flag = False

        self.video_active = False
        self.frame_queue = queue.Queue(maxsize=5)
        self.video_thread = None

        self.sdk.ASIGetNumOfConnectedCameras.restype = ctypes.c_int
        num = self.sdk.ASIGetNumOfConnectedCameras()
        if num <= 0:
            raise RuntimeError("未检测到 ASI 相机")

    def connect(self):
        if self.connected:
            return

        self._connecting = True
        try:
            # 清空旧的控制项，避免重复
            self.control_caps.clear()

            # 1. 获取相机信息
            res = self.sdk.ASIGetCameraProperty(ctypes.byref(self.info), self.camera_id)
            if res != 0:
                raise RuntimeError(f"ASIGetCameraProperty failed: {res}")

            # 2. 打开相机
            res = self.sdk.ASIOpenCamera(self.camera_id)
            if res != 0:
                raise RuntimeError(f"ASIOpenCamera failed: {res}")

            # 3. 初始化相机
            res = self.sdk.ASIInitCamera(self.camera_id)
            if res != 0:
                raise RuntimeError(f"ASIInitCamera failed: {res}")

            # 4. 获取控制参数数量
            count = ctypes.c_int()
            res = self.sdk.ASIGetNumOfControls(self.camera_id, ctypes.byref(count))
            if res != 0:
                raise RuntimeError(f"ASIGetNumOfControls failed: {res}")

            # 5. 获取所有控制项信息
            for i in range(count.value):
                cap = ASI_CONTROL_CAPS()
                res = self.sdk.ASIGetControlCaps(self.camera_id, i, ctypes.byref(cap))
                if res != 0:
                    raise RuntimeError(f"ASIGetControlCaps({i}) failed: {res}")
                self.control_caps.append(cap)

            # 6. 设置图像尺寸格式（默认最大分辨率 + bin = 1）
            res = self.sdk.ASISetROIFormat(
                self.camera_id,
                self.info.MaxWidth,
                self.info.MaxHeight,
                1,
                ASI_IMG_TYPE.ASI_IMG_RAW16
            )
            # 注意位深
            if res != 0:
                raise RuntimeError(f"ASISetROIFormat failed: {res}")

            # 7. 设置 ROI 起点（默认为左上角）
            res = self.sdk.ASISetStartPos(self.camera_id, 0, 0)
            if res != 0:
                raise RuntimeError(f"ASISetStartPos failed: {res}")

            # 标记连接成功
            self.connected = True
            self._state = CameraStates.cameraIdle

        except Exception as ex:
            self._state = CameraStates.cameraError
            raise
        finally:
            self._connecting = False

    def disconnect(self):
        if not self.connected:
            return
        '''
        Alpaca 规范其实允许 Disconnect 是同步的，因为释放硬件资源一般很快。
        如果要实现异步，可以在 Falcon handler 里用线程池或 asyncio.to_thread() 包装，但 SDK 本身没有异步接口，所以本质还是同步调用。
        我认为这里保持同步即可，不需要强行异步化，重点是保证状态标志正确。
        '''
        self._connecting = True
        try:
            res = self.sdk.ASICloseCamera(self.camera_id)
            if res != 0:
                raise RuntimeError(f"ASICloseCamera failed: {res}")

            # 清理状态
            self.connected = False
            self._state = CameraStates.cameraIdle
            self.control_caps.clear()
            self._num_x = self.info.MaxWidth
            self._num_y = self.info.MaxHeight
            self._start_x = 0
            self._start_y = 0

        finally:
            self._connecting = False

    def is_connected(self):
        return self.connected

    def is_connecting(self):
        return self._connecting

    @property
    def offset(self) -> int:
        for caps in self.control_caps:
            if caps.ControlType == ASI_CONTROL_TYPE.ASI_OFFSET:
                value = ctypes.c_long()
                auto = ctypes.c_int()
                self.sdk.ASIGetControlValue(self.camera_id, ASI_CONTROL_TYPE.ASI_OFFSET, ctypes.byref(value),
                                            ctypes.byref(auto))
                return value.value
        raise NotImplementedError("Offset 控制不支持")

    def set_offset(self, val: int):
        for caps in self.control_caps:
            if caps.ControlType == ASI_CONTROL_TYPE.ASI_OFFSET:
                if not (caps.MinValue <= val <= caps.MaxValue):
                    raise ValueError(f"Offset {val} 超出有效范围：{caps.MinValue}–{caps.MaxValue}")
                self.sdk.ASISetControlValue(self.camera_id, ASI_CONTROL_TYPE.ASI_OFFSET, ctypes.c_long(val),
                                            ASI_BOOL.ASI_FALSE)
                return
        raise NotImplementedError("Offset 控制不支持")

    @property
    def offset_min(self) -> int:
        for caps in self.control_caps:
            if caps.ControlType == ASI_CONTROL_TYPE.ASI_OFFSET:
                return caps.MinValue
        raise NotImplementedError("当前相机不支持 Offset 控制")

    @property
    def offset_max(self) -> int:
        for caps in self.control_caps:
            if caps.ControlType == ASI_CONTROL_TYPE.ASI_OFFSET:
                return caps.MaxValue
        raise NotImplementedError("Offset 控制不支持")

    @property
    def get_set_ccd_temperature(self) -> float:
        self.assert_cooler_supported()

        val = ctypes.c_long()
        auto = ctypes.c_int()
        self.sdk.ASIGetControlValue(self.camera_id, ASI_CONTROL_TYPE.ASI_TEMPERATURE, ctypes.byref(val),
                                    ctypes.byref(auto))
        return val.value / 10.0

    def set_ccd_temperature(self, temp_celsius: float):
        self.assert_cooler_supported()

        temp_val = int(temp_celsius * 10)
        self.sdk.ASISetControlValue(self.camera_id, ASI_CONTROL_TYPE.ASI_TEMPERATURE, temp_val, ASI_BOOL.ASI_FALSE)

    @property
    def start_x(self) -> int:
        return getattr(self, "_start_x", 0)

    @property
    def start_y(self) -> int:
        return getattr(self, "_start_y", 0)

    @property
    def sensor_type(self) -> SensorType:
        if not self.info.IsColorCam:
            return SensorType.Monochrome

        # 彩色相机，按 BayerPattern 分类
        if self.info.BayerPattern in [0, 1, 2, 3]:  # RGGB 系
            return SensorType.RGGB

        # 若未来 ZWO 支持其他 Bayer，可拓展
        return SensorType.Color  # fallback

    @property
    def sensor_name(self) -> str:
        # 示例: ASI462MC → 提取 "462MC"，或直接返回整机名称
        full_name = self.info.Name.decode("utf-8").strip()
        return full_name  # 或者：return full_name.replace("ASI", "")

    @property
    def pixel_size_x(self) -> float:
        return self.info.PixelSize

    @property
    def pixel_size_y(self) -> float:
        return self.info.PixelSize

    @property
    def num_x(self) -> int:
        return getattr(self, "_num_x", self.image_width)  # fallback to full size


    @property
    def num_y(self) -> int:
        return getattr(self, "_num_y", self.image_height)  # 默认使用最大高度

    @property
    def max_adu(self) -> int:
        if self.img_type in (ASI_IMG_TYPE.ASI_IMG_RAW8, ASI_IMG_TYPE.ASI_IMG_Y8):
            return 255
        elif self.img_type == ASI_IMG_TYPE.ASI_IMG_RAW16:
            return 65535
        elif self.img_type == ASI_IMG_TYPE.ASI_IMG_RGB24:
            return 255
        else:
            raise NotImplementedError(f"Unsupported image type: {self.img_type}")

    @property
    def bayer_offset_x(self) -> int:
        if not self.info.IsColorCam:
            return 0  # 或 raise Exception("Not a color camera")
        pattern = ASI_BAYER_PATTERN(self.info.BayerPattern)
        return 0 if pattern in [ASI_BAYER_PATTERN.ASI_BAYER_RG, ASI_BAYER_PATTERN.ASI_BAYER_GR] else 1

    @property
    def bayer_offset_y(self) -> int:
        if not self.info.IsColorCam:
            return 0  # 或 raise Exception("Not a color camera")
        pattern = ASI_BAYER_PATTERN(self.info.BayerPattern)
        return 0 if pattern in [ASI_BAYER_PATTERN.ASI_BAYER_RG, ASI_BAYER_PATTERN.ASI_BAYER_GB] else 1

    # def validate_roi(self):
    #     """
    #     校验并自动修正 ROI 参数，确保合法性。
    #     包括：
    #       - BinX/BinY 的支持性
    #       - NumX/NumY 是否在合法范围
    #       - StartX/StartY 是否越界
    #       - iWidth % 8 == 0，iHeight % 2 == 0
    #     """
    #
    #     # --- BinX / BinY 合法性 ---
    #     if self._bin_x <= 0 or self._bin_x not in self.supported_bins:
    #         raise ValueError(f"BinX {self._bin_x} not supported")
    #
    #     if self._bin_y <= 0 or self._bin_y not in self.supported_bins:
    #         raise ValueError(f"BinY {self._bin_y} not supported")
    #
    #     # --- NumX / NumY 范围检查 ---
    #     max_numx = self.info.MaxWidth // self._bin_x
    #     max_numy = self.info.MaxHeight // self._bin_y
    #
    #     if self._num_x <= 0 or self._num_x > max_numx:
    #         self._num_x = max_numx  # 自动修正
    #
    #     if self._num_y <= 0 or self._num_y > max_numy:
    #         self._num_y = max_numy  # 自动修正
    #
    #     # --- StartX / StartY 范围检查 ---
    #     max_startx = max_numx - self._num_x
    #     max_starty = max_numy - self._num_y
    #
    #     if self._start_x < 0:
    #         self._start_x = 0
    #     elif self._start_x > max_startx:
    #         self._start_x = max_startx
    #
    #     if self._start_y < 0:
    #         self._start_y = 0
    #     elif self._start_y > max_starty:
    #         self._start_y = max_starty
    #
    #     # --- 厂商要求的限制 ---
    #     # 宽度必须是8的倍数，高度必须是2的倍数
    #     if self._num_x % 8 != 0:
    #         self._num_x -= self._num_x % 8  # 向下修正到最近的 8 倍数
    #         if self._num_x <= 0:
    #             self._num_x = 8
    #
    #     if self._num_y % 2 != 0:
    #         self._num_y -= 1  # 向下修正为偶数
    #         if self._num_y <= 0:
    #             self._num_y = 2
    #
    #     # --- 更新 ROI 到 SDK ---
    #     res = self.sdk.ASISetROIFormat(
    #         self.camera_id,
    #         self._num_x,
    #         self._num_y,
    #         self._bin_x,
    #         self.img_type
    #     )
    #     if res != 0:
    #         raise RuntimeError(f"ASISetROIFormat failed: {res}")
    #
    #     res = self.sdk.ASISetStartPos(
    #         self.camera_id,
    #         self._start_x,
    #         self._start_y
    #     )
    #     if res != 0:
    #         raise RuntimeError(f"ASISetStartPos failed: {res}")

    # camera_asi.py —— 用下面内容替换原 validate_roi()

    def validate_roi(self):
        """
        校验并自动修正 ROI 参数，确保合法性。
        顺序要点：
          1) 先校验/修正 Bin 与 Num 的范围
          2) 再做 Num 的 8/2 对齐
          3) 用对齐后的 Num 重新计算 Start 合法范围并裁剪
          4) 最后一次性提交 SDK
        """

        # --- Bin 支持性 ---
        if self._bin_x <= 0 or self._bin_x not in self.supported_bins:
            raise ValueError(f"BinX {self._bin_x} not supported")
        if self._bin_y <= 0 or self._bin_y not in self.supported_bins:
            raise ValueError(f"BinY {self._bin_y} not supported")

        # 若设备不支持非对称 bin，这里确保一致（防御性）
        can_asym = False
        if not can_asym and self._bin_x != self._bin_y:
            # 选择较小的 bin，以提高兼容性；也可直接抛异常
            b = min(self._bin_x, self._bin_y)
            self._bin_x = self._bin_y = b

        # --- 新 bin 下的最大 binned 尺寸 ---
        max_numx = self.info.MaxWidth // self._bin_x
        max_numy = self.info.MaxHeight // self._bin_y

        # --- 先把 NumX/NumY 限制在最大范围内 ---
        self._num_x = max(1, min(self._num_x or max_numx, max_numx))
        self._num_y = max(1, min(self._num_y or max_numy, max_numy))

        # --- 厂商要求：对齐后尺寸 ---
        # 宽度必须是8的倍数，高度必须是2的倍数
        self._num_x -= (self._num_x % 8)
        if self._num_x <= 0: self._num_x = 8

        if (self._num_y % 2) != 0:
            self._num_y -= 1
        if self._num_y <= 0: self._num_y = 2

        # --- 用“对齐后的 Num”重新计算 Start 合法范围并裁剪 ---
        max_startx = (self.info.MaxWidth // self._bin_x) - self._num_x
        max_starty = (self.info.MaxHeight // self._bin_y) - self._num_y
        self._start_x = max(0, min(self._start_x, max_startx))
        self._start_y = max(0, min(self._start_y, max_starty))

        # --- 一次性下发 SDK ---
        res = self.sdk.ASISetROIFormat(self.camera_id, self._num_x, self._num_y, self._bin_x, self.img_type)
        if res != 0:
            raise RuntimeError(f"ASISetROIFormat failed: {res}")

        res = self.sdk.ASISetStartPos(self.camera_id, self._start_x, self._start_y)
        if res != 0:
            raise RuntimeError(f"ASISetStartPos failed: {res}")

    def _recompute_roi_for_new_bin(self):
        """
        在 bin 发生变化时，先按新 bin 计算合法范围与对齐要求，
        再裁剪 NumX/NumY 与 StartX/StartY，最后一次性下发给 SDK。
        """
        # 1) 新 bin 下的最大 binned 尺寸
        max_numx = self.info.MaxWidth // self._bin_x
        max_numy = self.info.MaxHeight // self._bin_y

        # 2) 先把 NumX/NumY 限制在最大范围内
        self._num_x = max(1, min(self._num_x or max_numx, max_numx))
        self._num_y = max(1, min(self._num_y or max_numy, max_numy))

        # 3) 按厂商要求对齐（宽8对齐，高2对齐）
        self._num_x -= (self._num_x % 8)
        if self._num_x <= 0: self._num_x = 8
        if (self._num_y % 2) != 0:
            self._num_y -= 1
        if self._num_y <= 0: self._num_y = 2

        # 4) 依据对齐后的 Num 重新计算 Start 合法范围并裁剪
        max_startx = max_numx - self._num_x
        max_starty = max_numy - self._num_y
        self._start_x = max(0, min(self._start_x, max_startx))
        self._start_y = max(0, min(self._start_y, max_starty))

        # 5) 一次性提交给 SDK
        res = self.sdk.ASISetROIFormat(self.camera_id, self._num_x, self._num_y, self._bin_x, self.img_type)
        if res != 0:
            raise RuntimeError(f"ASISetROIFormat failed: {res}")

        res = self.sdk.ASISetStartPos(self.camera_id, self._start_x, self._start_y)
        if res != 0:
            raise RuntimeError(f"ASISetStartPos failed: {res}")
    def set_binx(self, value: int):
        if value <= 0 or value not in self.supported_bins:
            raise ValueError(f"BinX {value} not supported")

        # 若设备不支持非对称 bin，则同时把 Y 设为相同值，避免中间态 (X!=Y)
        can_asym = False  # 与 camera.py 的 canasymmetricbin 一致；如将来可探测则替换为真实值
        if not can_asym:
            self._bin_x = value
            self._bin_y = value
            self._recompute_roi_for_new_bin()
        else:
            self._bin_x = value
            self.validate_roi()

    def set_biny(self, value: int):
        if value <= 0 or value not in self.supported_bins:
            raise ValueError(f"BinY {value} not supported")

        can_asym = False  # 与上保持一致
        if not can_asym:
            self._bin_y = value
            self._bin_x = value
            self._recompute_roi_for_new_bin()
        else:
            self._bin_y = value
            self.validate_roi()

    def set_num_x(self, value: int):
        if value <= 0:
            raise ValueError("NumX must be positive")
        self._num_x = value
        self.validate_roi()

    def set_num_y(self, value: int):
        if value <= 0:
            raise ValueError("NumY must be positive")
        self._num_y = value
        self.validate_roi()

    def set_start_x(self, value: int):
        if value < 0:
            raise ValueError("StartX must be >= 0")
        self._start_x = value
        self.validate_roi()

    def set_start_y(self, value: int):
        if value < 0:
            raise ValueError("StartY must be >= 0")
        self._start_y = value
        self.validate_roi()

    def get_binning_x(self) -> int:
        return self._bin_x

    def get_binning_y(self) -> int:
        return self._bin_y

    @property
    def supported_bins(self):  # -> list[int]
        raw_bins = self.info.SupportedBins  # 类型为 c_int_Array_16
        bins = []
        for val in raw_bins:
            if val == 0:
                break
            bins.append(val)
        return bins

    @property
    def max_bin_x(self) -> int:
        bins = self.supported_bins
        return max(bins) if bins else 1

    @property
    def max_bin_y(self) -> int:
        bins = self.supported_bins
        return max(bins) if bins else 1

    @property
    def camera_x_size(self) -> int:
        return self.info.MaxWidth

    @property
    def camera_y_size(self) -> int:
        return self.info.MaxHeight

    @property
    def can_abort_exposure(self) -> bool:
        return True  # 因为 ASIStopExposure() 是 SDK 标准接口

    @property
    def can_fast_readout(self) -> bool:
        # 如果控制项列表中有 ASI_HIGH_SPEED_MODE，则支持
        for cap in self.control_caps:
            if cap.ControlType == ASI_CONTROL_TYPE.ASI_HIGH_SPEED_MODE:
                return True
        return False

    @property
    def can_get_cooler_power(self) -> bool:
        self.assert_cooler_supported()

        for cap in self.control_caps:
            if cap.ControlType == ASI_CONTROL_TYPE.ASI_COOLER_POWER_PCT:
                return True
        return False

    @property
    def can_pulse_guide(self) -> bool:
        return bool(self.info.ST4Port)

    @property
    def can_set_ccd_temperature(self) -> bool:
        return bool(self.info.IsCoolerCam)

    @property
    def can_stop_exposure(self) -> bool:
        # 一般情况下，ZWO 相机可提前停止并读出图像
        # 和中止曝光不同的是，一个可以读未曝光完成的数据，另一个不行
        return True

    def assert_cooler_supported(self):
        if not self.info.IsCoolerCam:
            raise NotImplementedError("当前相机不支持制冷功能")

    @property
    def ccd_temperature(self) -> float:
        temp_value = ctypes.c_long()
        auto = ctypes.c_int()

        res = self.sdk.ASIGetControlValue(
            self.camera_id,
            ASI_CONTROL_TYPE.ASI_TEMPERATURE,
            ctypes.byref(temp_value),
            ctypes.byref(auto)
        )
        if res != 0:
            raise RuntimeError(f"ASIGetControlValue(ASI_TEMPERATURE) failed: {res}")

        return temp_value.value / 10.0  # 转换为°C

    @property
    def cooler_on(self) -> bool:
        self.assert_cooler_supported()

        value = ctypes.c_long()
        auto = ctypes.c_int()
        res = self.sdk.ASIGetControlValue(
            self.camera_id,
            ASI_CONTROL_TYPE.ASI_COOLER_ON,
            ctypes.byref(value),
            ctypes.byref(auto)
        )
        if res != 0:
            raise RuntimeError("ASIGetControlValue(ASI_COOLER_ON) failed")
        return value.value != 0

    def set_cooler_on(self, state: bool):
        self.assert_cooler_supported()

        val = 1 if state else 0
        res = self.sdk.ASISetControlValue(
            self.camera_id,
            ASI_CONTROL_TYPE.ASI_COOLER_ON,
            ctypes.c_long(val),
            ASI_BOOL.ASI_FALSE
        )

        if res != 0:
            raise RuntimeError("ASISetControlValue(ASI_COOLER_ON) failed")

    @property
    def cooler_power(self) -> float:
        self.assert_cooler_supported()

        val = ctypes.c_long()
        auto = ctypes.c_int()
        res = self.sdk.ASIGetControlValue(
            self.camera_id,
            ASI_CONTROL_TYPE.ASI_COOLER_POWER_PCT,
            ctypes.byref(val),
            ctypes.byref(auto)
        )
        if res != 0:
            raise RuntimeError("ASIGetControlValue(ASI_COOLER_POWER_PCT) failed")
        return val.value / 10.0  # 转为百分比

    @property
    def electrons_per_adu(self) -> float:
        return self.info.ElecPerADU

    @property
    def exposure_max(self) -> float:
        for cap in self.control_caps:
            if cap.ControlType == ASI_CONTROL_TYPE.ASI_EXPOSURE:
                return cap.MaxValue / 1_000_000.0  # μs → s
        raise RuntimeError("ASI_EXPOSURE control not found")

    @property
    def exposure_min(self) -> float:
        for cap in self.control_caps:
            if cap.ControlType == ASI_CONTROL_TYPE.ASI_EXPOSURE:
                return cap.MinValue / 1_000_000.0  # μs → s
        raise RuntimeError("ASI_EXPOSURE control not found")

    @property
    def exposure_resolution(self) -> float:
        return 1e-6  # ZWO 相机最小单位为 1 微秒

    @property
    def fast_readout(self) -> bool:
        val = ctypes.c_long()
        auto = ctypes.c_int()
        res = self.sdk.ASIGetControlValue(
            self.camera_id,
            ASI_CONTROL_TYPE.ASI_HIGH_SPEED_MODE,
            ctypes.byref(val),
            ctypes.byref(auto)
        )
        if res != 0:
            raise RuntimeError("ASIGetControlValue(ASI_HIGH_SPEED_MODE) failed")
        return bool(val.value)

    def set_fast_readout(self, enable: bool):
        val = 1 if enable else 0
        res = self.sdk.ASISetControlValue(
            self.camera_id,
            ASI_CONTROL_TYPE.ASI_HIGH_SPEED_MODE,
            ctypes.c_long(val),
            ASI_BOOL.ASI_FALSE
        )
        if res != 0:
            raise RuntimeError("ASISetControlValue(ASI_HIGH_SPEED_MODE) failed")

    @property
    def full_well_capacity(self) -> int:
        # ZWO 官网或 SDK 外部文档获得精确值，也可以直接替换掉这个估算方式
        bit_depth = self.info.BitDepth  # e.g. 12, 14
        electrons_per_adu = self.info.ElecPerADU  # e.g. 0.24
        full_well = (2 ** bit_depth) * electrons_per_adu
        return int(full_well)

    @property
    def gain_range(self):  # -> tuple[(int, int)]
        for cap in self.control_caps:
            if cap.ControlType == ASI_CONTROL_TYPE.ASI_GAIN:
                return cap.MinValue, cap.MaxValue
        return (0, 600)  # fallback

    @property
    def offset_range(self):  # -> tuple[(int, int)]
        for cap in self.control_caps:
            if cap.ControlType == ASI_CONTROL_TYPE.ASI_OFFSET:
                return cap.MinValue, cap.MaxValue
        return (0, 600)  # fallback

    @property
    def gain(self) -> int:
        val = ctypes.c_long()
        auto = ctypes.c_int()
        res = self.sdk.ASIGetControlValue(
            self.camera_id,
            ASI_CONTROL_TYPE.ASI_GAIN,
            ctypes.byref(val),
            ctypes.byref(auto)
        )
        if res != 0:
            raise RuntimeError("ASIGetControlValue(GAIN) failed")
        return val.value

    def set_gain(self, value: int):

        res = self.sdk.ASISetControlValue(
            self.camera_id,
            ASI_CONTROL_TYPE.ASI_GAIN,
            ctypes.c_long(value),
            ASI_BOOL.ASI_FALSE
        )
        if res != 0:
            raise RuntimeError("ASISetControlValue(GAIN) failed")

    @property
    def gain_max(self) -> int:
        for cap in self.control_caps:
            if cap.ControlType == ASI_CONTROL_TYPE.ASI_GAIN:
                return cap.MaxValue
        raise RuntimeError("ASI_GAIN 控制项未找到")

    @property
    def gain_min(self) -> int:
        for cap in self.control_caps:
            if cap.ControlType == ASI_CONTROL_TYPE.ASI_GAIN:
                return cap.MinValue
        raise RuntimeError("ASI_GAIN 控制项未找到")

    @property
    def gain_modes(self):
        # ZWO 不支持命名 gain 档，返回空列表代表“非索引模式” -> list[str]
        return []

    @property
    def has_shutter(self) -> bool:
        return bool(self.info.MechanicalShutter)

    @property
    def heatsink_temperature(self) -> float:
        raise NotImplementedError("该相机不支持独立 HeatsinkTemperature 读取")

    @property
    def image_ready(self) -> bool:
        """
        曝光是否完成，图像是否可读。
        """
        return self.image_ready_flag

    def pulse_guide(self, direction: ASI_GUIDE_DIRECTION, duration_ms: int):
        res = self.sdk.ASIPulseGuideOn(self.camera_id, direction)
        if res != 0:
            raise RuntimeError(f"ASIPulseGuideOn failed: {res}")

        self._is_pulse_guiding = True

        def stop():
            self.sdk.ASIPulseGuideOff(self.camera_id, direction)
            self._is_pulse_guiding = False

        t = threading.Timer(duration_ms / 1000.0, stop)
        t.start()

    @property
    def is_pulse_guiding(self) -> bool:
        return getattr(self, "_is_pulse_guiding", False)

    @property
    def last_exposure_duration(self) -> float:
        if hasattr(self, "_exposure_start_time") and hasattr(self, "_exposure_end_time"):
            return self._exposure_end_time - self._exposure_start_time
        return getattr(self, "_exposure_duration_sec", 0.0)  # fallback

    @property
    def last_exposure_start_time(self) -> str:
        if hasattr(self, "_exposure_start_time"):
            return self._exposure_start_time1.isoformat() + "Z"  # UTC 标记
        return ""

    @property
    def camera_state(self):
        return self._state

    def _exposure_poll_loop(self):
        """后台线程：周期性检查曝光状态"""
        try:
            exp_status = ctypes.c_int()
            while self._polling_active:

                res = self.sdk.ASIGetExpStatus(self.camera_id, ctypes.byref(exp_status))
                if res != 0:
                    self._state = CameraStates.cameraError
                    break

                status = exp_status.value

                if status == ASI_EXPOSURE_STATUS.ASI_EXP_IDLE:
                    # 空闲状态
                    self._state = CameraStates.cameraIdle
                    self.image_ready_flag = False

                elif status == ASI_EXPOSURE_STATUS.ASI_EXP_WORKING:
                    # 曝光进行中
                    self._state = CameraStates.cameraExposing
                    self.image_ready_flag = False

                elif status == ASI_EXPOSURE_STATUS.ASI_EXP_SUCCESS:
                    # 曝光完成，进入读出阶段
                    self._state = CameraStates.cameraReading
                    print('finnish cameraReading')

                    # 标记图像已准备好，供 ImageReady 使用
                    self.image_ready_flag = True

                    # 暂时停在 Reading，等待 ImageArray/ImageBytes 调用时进入 Download
                    # break

                elif status == ASI_EXPOSURE_STATUS.ASI_EXP_FAILED:
                    self._state = CameraStates.cameraError
                    self.image_ready_flag = False
                    break

                time.sleep(0.01)  # 每 10ms 轮询一次

        except Exception as e:
            self._state = CameraStates.cameraError

        finally:
            self._polling_active = False

    def duration_check(self, exposure_time_s):
        if exposure_time_s <= 0.000_001 or exposure_time_s > 3600:
            raise ValueError("曝光时间超出范围 (0–3600 秒)")

    def start_exposure(self, exposure_time_s: float, light: bool):
        # 检查状态
        if self._state == CameraStates.cameraExposing:
            raise RuntimeError("Exposure already in progress")

        # 记录状态
        self._state = CameraStates.cameraExposing
        self._exposure_duration_sec = exposure_time_s
        self._exposure_start_time = time.time()
        self._exposure_start_time1 = datetime.datetime.utcnow()

        # 设置 ROI 和图像格式（每次曝光前都调用，保证 SDK 内部状态正确）
        res = self.sdk.ASISetROIFormat(
            self.camera_id,
            self._num_x,
            self._num_y,
            self._bin_x,
            self.img_type
        )
        print(f"[DEBUG] ASISetROIFormat returned {res}")

        res = self.sdk.ASISetStartPos(self.camera_id, self._start_x, self._start_y)
        print(f"[DEBUG] ASISetStartPos returned {res}")


        # 设置曝光时间 (µs)
        exposure_us = int(exposure_time_s * 1_000_000)
        res = self.sdk.ASISetControlValue(
            self.camera_id,
            ASI_CONTROL_TYPE.ASI_EXPOSURE,
            exposure_us,
            ASI_BOOL.ASI_FALSE
        )
        if res != 0:
            self._state = CameraStates.cameraError
            raise RuntimeError("ASISetControlValue(EXPOSURE) failed")

        # 开始曝光，区分 Light/Dark
        b_is_dark = ASI_BOOL.ASI_FALSE if light else ASI_BOOL.ASI_TRUE
        res = self.sdk.ASIStartExposure(self.camera_id, b_is_dark)

        if res != 0:
            self._state = CameraStates.cameraError
            raise RuntimeError("ASIStartExposure failed")

        # 启动轮询线程
        self._polling_active = True
        self._polling_thread = threading.Thread(
            target=self._exposure_poll_loop, daemon=True
        )
        self._polling_thread.start()

        print('12345AAAA')
        self.image_ready_flag = False

    def stop_exposure(self):
        res = self.sdk.ASIStopExposure(self.camera_id)
        if res != 0:
            raise RuntimeError("ASIStopExposure failed")

    def mark_exposure_complete(self):
        self._exposure_end_time = time.time()

    def readout_image(self):
        self._state = CameraStates.cameraReading
        # 读取图像...

    def finish_download(self):
        self._state = CameraStates.cameraDownload
        self._state = CameraStates.cameraIdle

    @property
    def image_array(self):  # -> list[list[int]]
        if not self.image_ready:
            raise RuntimeError("Image is not ready")


        self._state = CameraStates.cameraIdle
        self.image_ready_flag = False
        self._polling_active = False  # ✅ 通知轮询线程可以退出


        width = self._num_x
        height = self._num_y
        img_type = self.img_type

        pixel_size = {
            ASI_IMG_TYPE.ASI_IMG_RAW8: 1,
            ASI_IMG_TYPE.ASI_IMG_RAW16: 2,
            ASI_IMG_TYPE.ASI_IMG_RGB24: 3,
            ASI_IMG_TYPE.ASI_IMG_Y8: 1,
        }[img_type]

        bufsize = width * height * pixel_size
        buffer = (ctypes.c_ubyte * bufsize)()

        res = self.sdk.ASIGetDataAfterExp(
            self.camera_id,
            buffer,
            bufsize,
        )
        # 添加日志：ASIGetDataAfterExp 返回值
        print(f"[ZwoCamera.image_array] ASIGetDataAfterExp returned: {res}")
        if res != 0:
            raise RuntimeError(f"ASIGetImageData failed with error code: {res}")

        # 将缓冲区转换为普通的 Python 列表
        flat = list(buffer)

        # 添加日志：Flat list length: {len(flat)}")
        print(flat[:20])
        # 下采样因子 - 如果图像很小，就不要下采样了
        step = 1

        reduced = []
        try:
            if pixel_size == 1:
                # RAW8 / Y8
                for y in range(0, height, step):
                    row = []
                    base = y * width
                    for x in range(0, width, step):
                        row.append(flat[base + x])
                    reduced.append(row)
            elif pixel_size == 2:
                # RAW16：每两个字节一个像素 (小端序)
                for y in range(0, height, step):
                    row = []
                    base = y * width * 2  # 每行字节数
                    for x in range(0, width, step):
                        idx = base + (x * 2)
                        # 确保不越界
                        if idx + 1 < len(flat):
                            # 从两个字节组合成16位整数
                            val = flat[idx] | (flat[idx + 1] << 8)
                            row.append(val)
                        else:
                            row.append(0)  # 或者处理错误
                    reduced.append(row)
            elif pixel_size == 3:
                # RGB24 - 对于 /imagearray，通常返回单通道（例如亮度）。
                # 这里简单地将RGB转换为灰度：使用标准公式 0.299*R + 0.587*G + 0.114*B
                for y in range(0, height, step):
                    row = []
                    base = y * width * 3
                    for x in range(0, width, step):
                        idx = base + (x * 3)
                        if idx + 2 < len(flat):
                            R = flat[idx]
                            G = flat[idx + 1]
                            B = flat[idx + 2]
                            gray = int(0.299 * R + 0.587 * G + 0.114 * B)
                            row.append(gray)
                        else:
                            row.append(0)
                    reduced.append(row)
            else:
                raise NotImplementedError(f"Unsupported pixel size: {pixel_size}")
        except IndexError as e:
            print(f"[ZwoCamera.image_array] CRITICAL: IndexError during image data processing: {e}")
            print(
                f"[ZwoCamera.image_array] Details: len(flat)={len(flat)}, width={width}, height={height}, step={step}, pixel_size={pixel_size}")
            raise RuntimeError(
                "Failed to process image data due to index error. Buffer size might be incorrect.") from e

        print(
            f"[ZwoCamera.image_array] Successfully created reduced image array: {len(reduced)}x{len(reduced[0]) if reduced else 0}")

        return reduced

    @property
    def imagearrayinfo(self) -> dict:
        """
        返回图像数组的结构信息，符合 Alpaca ImageArrayInfo 规范。
        动态根据当前的 self.img_type 决定 ElementType。
        """

        width = self._num_x
        height = self._num_y
        img_type = self.img_type

        # Alpaca ImageArrayElementTypes 枚举对应值：
        # Unknown=0, Int16=1, Int32=2, Double=3, Single=4,
        # UInt64=5, Byte=6, Int64=7, UInt16=8
        element_type_map = {
            ASI_IMG_TYPE.ASI_IMG_RAW8: 6,  # Byte
            ASI_IMG_TYPE.ASI_IMG_RAW16: 8,  # UInt16
            ASI_IMG_TYPE.ASI_IMG_RGB24: 6,  # 每通道 Byte
            ASI_IMG_TYPE.ASI_IMG_Y8: 6,  # Byte
        }

        if img_type not in element_type_map:
            raise RuntimeError(f"Unsupported image type for ImageArrayInfo: {img_type}")

        elem_type = element_type_map[img_type]

        metadata = {
            "MetadataVersion": 1,
            "ImageElementType": 2,
            "TransmissionElementType": 8,
            "Rank": 2 if img_type != ASI_IMG_TYPE.ASI_IMG_RGB24 else 3,
            "Dimension1": width,  # 行数
            "Dimension2": height,  # 列数
            "Dimension3": 0,
        }

        return metadata

    @property
    def image_bytes(self) -> bytes:
        if not self.image_ready:
            raise RuntimeError("Image not ready")

        width = self._num_x
        height = self._num_y
        img_type = self.img_type

        # 计算像素大小 + Alpaca 元素类型
        if img_type == ASI_IMG_TYPE.ASI_IMG_RAW8:
            elem_type = 6  # Byte
            pixel_size = 1
        elif img_type == ASI_IMG_TYPE.ASI_IMG_RAW16:
            elem_type = 8  # UInt16
            pixel_size = 2
        elif img_type == ASI_IMG_TYPE.ASI_IMG_RGB24:
            elem_type = 6  # Byte (每通道)
            pixel_size = 3
        else:
            raise RuntimeError("Unsupported image type")

        bufsize = width * height * pixel_size
        buffer = (ctypes.c_ubyte * bufsize)()

        res = self.sdk.ASIGetDataAfterExp(self.camera_id, buffer, bufsize)
        if res != 0:
            raise RuntimeError(f"ASIGetDataAfterExp failed: {res}")

        # 构造 Alpaca ImageBytes 头部（92 字节）
        header = bytearray(92)

        def put(offset, val, size=4):
            header[offset:offset + size] = int(val).to_bytes(size, 'little')

        put(0, 1)  # MetaVersion
        put(4, 0)  # ErrorNumber
        put(8, 0)  # Reserved1
        put(12, 0)  # Reserved2
        put(16, 92)  # ImageDataStart
        put(20, elem_type)  # ImageElementType
        put(24, elem_type)  # TransmissionElementType
        put(28, 2 if pixel_size != 3 else 3)  # Rank
        put(32, height)  # Dimension1
        put(36, width)  # Dimension2
        put(40, 3 if pixel_size == 3 else 1)  # Dimension3 (灰度=1, 彩色=3)

        # 状态清理
        self.image_ready_flag = False
        self._state = CameraStates.cameraIdle

        return bytes(header) + bytes(buffer)

    def Action(self, actionName: str, parameters: str):
        name = actionName.lower()
        if name == "startcontinuousexposure":
            # parameters 可以传曝光时间，单位秒
            try:
                exp_time = float(parameters)
                self.set_ExposureTime(exp_time)
            except Exception:
                pass  # 如果没传，就用已有值
            return self._start_video()

        elif name == "stopcontinuousexposure":
            return self._stop_video()

        elif name == "setvideoexposure":
            exp_time = float(parameters)
            self.set_ExposureTime(exp_time)
            return f"Exposure updated to {exp_time}s"

        else:
            raise InvalidOperationException(f"Unsupported action {actionName}")

    def set_ExposureTime(self, value: float):
        exposure_us = int(value * 1_000_000)
        res = self.sdk.ASISetControlValue(
            self.camera_id,
            ASI_CONTROL_TYPE.ASI_EXPOSURE,
            exposure_us,
            ASI_BOOL.ASI_FALSE
        )
        if res != 0:
            raise RuntimeError(f"Failed to set exposure: {res}")

        self._exposure_duration_sec = value
        print(f"[Video] Exposure updated to {value}s")

    # ---------- Video 模式 ----------
    def _start_video(self):
            if self.video_active:
                print('Already running')
                return "Already running"

            print('start video')


            res = self.sdk.ASIStartVideoCapture(self.camera_id)
            if res != 0:
                raise RuntimeError(f"ASIStartVideoCapture failed: {res}")

            self.video_active = True
            self.video_thread = threading.Thread(target=self._video_loop, daemon=True)
            self.video_thread.start()
            return "OK"

    def _stop_video(self):
        if not self.video_active:
            return "Not running"
        print('stop video')
        res = self.sdk.ASIStopVideoCapture(self.camera_id)
        if res != 0:
            raise RuntimeError(f"ASIStopVideoCapture failed: {res}")

        self.video_active = False
        return "OK"

    def _video_loop(self):
        width, height = self._num_x, self._num_y

        # 1. 根据图像类型计算 pixel_size
        if self.img_type == ASI_IMG_TYPE.ASI_IMG_RAW8:
            pixel_size = 1
        elif self.img_type == ASI_IMG_TYPE.ASI_IMG_RAW16:
            pixel_size = 2
        elif self.img_type == ASI_IMG_TYPE.ASI_IMG_RGB24:
            pixel_size = 3
        else:
            raise RuntimeError("Unsupported image type in video mode")

        bufsize = width * height * pixel_size
        buffer = (ctypes.c_ubyte * bufsize)()

        # 2. 计算合理的等待时间 (ms)
        waitms = int(self._exposure_duration_sec * 2000 + 500)

        while self.video_active:
            res = self.sdk.ASIGetVideoData(self.camera_id, buffer, bufsize, waitms)
            if res == 0:
                try:
                    # 3. 如果队列满 → 丢弃旧帧，保证实时性
                    if self.frame_queue.full():
                        _ = self.frame_queue.get_nowait()
                    self.frame_queue.put_nowait(bytes(buffer))
                except Exception:
                    pass  # 忽略队列错误
            else:
                # 如果采集失败 → 短暂休眠避免死循环
                time.sleep(0.01)

    def get_video_frame(self) -> bytes:
        try:
            raw = self.frame_queue.get(timeout=1)
        except queue.Empty:
            raise InvalidOperationException("No video frame available")

        return self._wrap_imagebytes(raw)

    # ---------- 封装成 ImageBytes ----------
    def _wrap_imagebytes(self, raw: bytes) -> bytes:
        width, height = self._num_x, self._num_y
        header = bytearray(92)

        def put(offset, val, size=4):
            header[offset:offset + size] = int(val).to_bytes(size, "little")

        put(0, 1)  # MetaVersion
        put(4, 0)  # ErrorNumber
        put(16, 92)  # ImageDataStart
        put(20, 8)  # ImageElementType (UInt16)
        put(24, 8)  # TransmissionElementType
        put(28, 2)  # Rank
        put(32, height)
        put(36, width)
        put(40, 1)  # Dimension3 (灰度)

        return bytes(header) + raw

